"""
Dynamic call detection for TypeScript code.

Detects dynamic function calls, eval usage, reflection patterns, and runtime
code execution that may be difficult to analyze statically.
"""

from typing import Dict, List, Any, Optional, Set, Tuple
from collections import defaultdict
import re

from ...core.error_handler import AnalysisError, log_info


class TypeScriptDynamicCallDetector:
    """Detects dynamic calls and runtime code execution patterns in TypeScript."""
    
    def __init__(self):
        """Initialize the dynamic call detector."""
        self.dynamic_patterns = []
        self.reflection_calls = []
        self.eval_usage = []
        self.dynamic_imports = []
    
    def detect_dynamic_calls(self, file_content: str, file_path: str) -> Dict[str, Any]:
        """Detect various forms of dynamic calls in TypeScript code."""
        try:
            eval_calls = self._detect_eval_usage(file_content)
            dynamic_imports = self._detect_dynamic_imports(file_content)
            reflection_calls = self._detect_reflection_patterns(file_content)
            computed_access = self._detect_computed_property_access(file_content)
            indirect_calls = self._detect_indirect_function_calls(file_content)
            template_calls = self._detect_template_based_calls(file_content)
            
            return {
                'file_path': file_path,
                'eval_calls': eval_calls,
                'dynamic_imports': dynamic_imports,
                'reflection_calls': reflection_calls,
                'computed_property_access': computed_access,
                'indirect_function_calls': indirect_calls,
                'template_based_calls': template_calls,
                'risk_assessment': self._assess_dynamic_call_risks(
                    eval_calls, dynamic_imports, reflection_calls, 
                    computed_access, indirect_calls, template_calls
                )
            }
            
        except Exception as e:
            raise AnalysisError(f"Failed to detect dynamic calls in {file_path}: {str(e)}")
    
    def _detect_eval_usage(self, content: str) -> List[Dict[str, Any]]:
        """Detect eval() and Function() constructor usage."""
        eval_calls = []
        
        # Patterns for eval-like constructs
        eval_patterns = [
            r'\beval\s*\(',
            r'\bFunction\s*\(',
            r'new\s+Function\s*\(',
            r'setTimeout\s*\(\s*[\'"]',  # String-based setTimeout
            r'setInterval\s*\(\s*[\'"]',  # String-based setInterval
        ]
        
        lines = content.split('\n')
        for line_num, line in enumerate(lines, 1):
            for pattern in eval_patterns:
                matches = re.finditer(pattern, line, re.IGNORECASE)
                for match in matches:
                    eval_type = self._classify_eval_usage(match.group(0))
                    
                    eval_calls.append({
                        'type': eval_type,
                        'line': line_num,
                        'column': match.start(),
                        'context': line.strip(),
                        'risk_level': self._assess_eval_risk(eval_type, line),
                        'pattern': match.group(0)
                    })
        
        return eval_calls
    
    def _detect_dynamic_imports(self, content: str) -> List[Dict[str, Any]]:
        """Detect dynamic import() expressions."""
        dynamic_imports = []
        
        # Dynamic import patterns
        import_patterns = [
            r'import\s*\(\s*([^)]+)\s*\)',
            r'await\s+import\s*\(\s*([^)]+)\s*\)',
            r'require\.resolve\s*\(\s*([^)]+)\s*\)',
        ]
        
        lines = content.split('\n')
        for line_num, line in enumerate(lines, 1):
            for pattern in import_patterns:
                matches = re.finditer(pattern, line)
                for match in matches:
                    module_expr = match.group(1).strip()
                    
                    dynamic_imports.append({
                        'type': 'dynamic_import',
                        'module_expression': module_expr,
                        'line': line_num,
                        'column': match.start(),
                        'context': line.strip(),
                        'is_variable': self._is_variable_import(module_expr),
                        'is_computed': self._is_computed_import(module_expr)
                    })
        
        return dynamic_imports
    
    def _detect_reflection_patterns(self, content: str) -> List[Dict[str, Any]]:
        """Detect reflection and metaprogramming patterns."""
        reflection_calls = []
        
        # Reflection patterns
        reflection_patterns = [
            r'Object\.getPrototypeOf\s*\(',
            r'Object\.getOwnPropertyNames\s*\(',
            r'Object\.getOwnPropertyDescriptors\s*\(',
            r'Object\.keys\s*\(',
            r'Object\.values\s*\(',
            r'Object\.entries\s*\(',
            r'Reflect\.\w+\s*\(',
            r'Proxy\s*\(',
            r'new\s+Proxy\s*\(',
            r'\.hasOwnProperty\s*\(',
            r'\.propertyIsEnumerable\s*\(',
            r'Object\.defineProperty\s*\(',
            r'Object\.create\s*\(',
        ]
        
        lines = content.split('\n')
        for line_num, line in enumerate(lines, 1):
            for pattern in reflection_patterns:
                matches = re.finditer(pattern, line)
                for match in matches:
                    reflection_type = self._classify_reflection_call(match.group(0))
                    
                    reflection_calls.append({
                        'type': reflection_type,
                        'line': line_num,
                        'column': match.start(),
                        'context': line.strip(),
                        'pattern': match.group(0),
                        'risk_level': self._assess_reflection_risk(reflection_type)
                    })
        
        return reflection_calls
    
    def _detect_computed_property_access(self, content: str) -> List[Dict[str, Any]]:
        """Detect computed property access patterns."""
        computed_access = []
        
        # Computed property patterns
        computed_patterns = [
            r'(\w+)\[([^\]]+)\]',  # obj[property]
            r'(\w+)\[([\'"][^\'"]*[\'"])\]',  # obj['property']
            r'(\w+)\[(\w+\s*\+[^]]+)\]',  # obj[prop + 'suffix']
        ]
        
        lines = content.split('\n')
        for line_num, line in enumerate(lines, 1):
            for pattern in computed_patterns:
                matches = re.finditer(pattern, line)
                for match in matches:
                    object_name = match.group(1)
                    property_expr = match.group(2)
                    
                    is_dynamic = self._is_dynamic_property_access(property_expr)
                    
                    if is_dynamic:
                        computed_access.append({
                            'object': object_name,
                            'property_expression': property_expr,
                            'line': line_num,
                            'column': match.start(),
                            'context': line.strip(),
                            'is_string_literal': property_expr.startswith('"') or property_expr.startswith("'"),
                            'is_computed': '+' in property_expr or self._contains_variables(property_expr)
                        })
        
        return computed_access
    
    def _detect_indirect_function_calls(self, content: str) -> List[Dict[str, Any]]:
        """Detect indirect function calls through variables or properties."""
        indirect_calls = []
        
        # Patterns for indirect calls
        indirect_patterns = [
            r'(\w+)\[([^\]]+)\]\s*\(',  # obj[method]()
            r'(\w+)\.(\w+)\.call\s*\(',  # func.call()
            r'(\w+)\.(\w+)\.apply\s*\(',  # func.apply()
            r'(\w+)\.(\w+)\.bind\s*\(',  # func.bind()
        ]
        
        lines = content.split('\n')
        for line_num, line in enumerate(lines, 1):
            for pattern in indirect_patterns:
                matches = re.finditer(pattern, line)
                for match in matches:
                    call_type = self._classify_indirect_call(match.group(0))
                    
                    indirect_calls.append({
                        'type': call_type,
                        'line': line_num,
                        'column': match.start(),
                        'context': line.strip(),
                        'pattern': match.group(0),
                        'object': match.group(1),
                        'method': match.group(2) if len(match.groups()) > 1 else None
                    })
        
        return indirect_calls
    
    def _detect_template_based_calls(self, content: str) -> List[Dict[str, Any]]:
        """Detect calls constructed through template literals or string manipulation."""
        template_calls = []
        
        # Template literal patterns that might construct function names
        template_patterns = [
            r'(\w+)\[`([^`]+)`\]',  # obj[`template`]
            r'(\w+)\[([^]]*\$\{[^}]+\}[^]]*)\]',  # obj[template with ${variable}]
            r'window\[([^]]*\$\{[^}]+\}[^]]*)\]',  # window[template]
        ]
        
        lines = content.split('\n')
        for line_num, line in enumerate(lines, 1):
            for pattern in template_patterns:
                matches = re.finditer(pattern, line)
                for match in matches:
                    template_calls.append({
                        'type': 'template_based_call',
                        'object': match.group(1),
                        'template': match.group(2),
                        'line': line_num,
                        'column': match.start(),
                        'context': line.strip(),
                        'has_variables': '${' in match.group(2)
                    })
        
        return template_calls
    
    def _classify_eval_usage(self, eval_call: str) -> str:
        """Classify the type of eval usage."""
        eval_call_lower = eval_call.lower()
        
        if 'function' in eval_call_lower:
            return 'function_constructor'
        elif 'settimeout' in eval_call_lower:
            return 'string_timeout'
        elif 'setinterval' in eval_call_lower:
            return 'string_interval'
        else:
            return 'direct_eval'
    
    def _classify_reflection_call(self, reflection_call: str) -> str:
        """Classify the type of reflection call."""
        call_lower = reflection_call.lower()
        
        if 'proxy' in call_lower:
            return 'proxy'
        elif 'reflect' in call_lower:
            return 'reflect_api'
        elif 'getprototypeof' in call_lower:
            return 'prototype_access'
        elif 'getownpropertynames' in call_lower or 'getownpropertydescriptors' in call_lower:
            return 'property_enumeration'
        elif 'defineproperty' in call_lower:
            return 'property_definition'
        elif 'keys' in call_lower or 'values' in call_lower or 'entries' in call_lower:
            return 'object_iteration'
        else:
            return 'general_reflection'
    
    def _classify_indirect_call(self, call: str) -> str:
        """Classify the type of indirect call."""
        if '.call' in call:
            return 'function_call'
        elif '.apply' in call:
            return 'function_apply'
        elif '.bind' in call:
            return 'function_bind'
        elif '[' in call and '](' in call:
            return 'computed_method_call'
        else:
            return 'unknown_indirect'
    
    def _is_variable_import(self, module_expr: str) -> bool:
        """Check if import uses a variable for the module path."""
        # Remove quotes to check if it's a literal string
        cleaned = module_expr.strip().strip('"\'')
        return module_expr.strip().startswith(('`', '"', "'")) is False
    
    def _is_computed_import(self, module_expr: str) -> bool:
        """Check if import uses computed expression."""
        return any(op in module_expr for op in ['+', '${', '`']) or self._contains_variables(module_expr)
    
    def _is_dynamic_property_access(self, property_expr: str) -> bool:
        """Check if property access is dynamic."""
        # Static string literals are not dynamic
        if (property_expr.startswith('"') and property_expr.endswith('"')) or \
           (property_expr.startswith("'") and property_expr.endswith("'")):
            return '+' in property_expr or self._contains_variables(property_expr)
        
        # Everything else is dynamic
        return True
    
    def _contains_variables(self, expression: str) -> bool:
        """Check if expression contains variables."""
        # Look for identifiers that aren't string literals
        return bool(re.search(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', expression.strip('"\'`')))
    
    def _assess_eval_risk(self, eval_type: str, context: str) -> str:
        """Assess the security risk of eval usage."""
        if eval_type in ['direct_eval', 'function_constructor']:
            return 'high'
        elif eval_type in ['string_timeout', 'string_interval']:
            return 'medium'
        else:
            return 'low'
    
    def _assess_reflection_risk(self, reflection_type: str) -> str:
        """Assess the risk level of reflection usage."""
        high_risk = ['proxy', 'property_definition']
        medium_risk = ['reflect_api', 'prototype_access']
        
        if reflection_type in high_risk:
            return 'high'
        elif reflection_type in medium_risk:
            return 'medium'
        else:
            return 'low'
    
    def _assess_dynamic_call_risks(self, eval_calls: List[Dict[str, Any]], 
                                  dynamic_imports: List[Dict[str, Any]],
                                  reflection_calls: List[Dict[str, Any]],
                                  computed_access: List[Dict[str, Any]],
                                  indirect_calls: List[Dict[str, Any]],
                                  template_calls: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Assess overall dynamic call risks."""
        total_dynamic_calls = (len(eval_calls) + len(dynamic_imports) + 
                              len(reflection_calls) + len(computed_access) + 
                              len(indirect_calls) + len(template_calls))
        
        high_risk_count = sum(1 for call in eval_calls if call.get('risk_level') == 'high')
        high_risk_count += sum(1 for call in reflection_calls if call.get('risk_level') == 'high')
        
        security_score = self._calculate_security_score(
            eval_calls, reflection_calls, dynamic_imports, 
            computed_access, indirect_calls, template_calls
        )
        
        return {
            'total_dynamic_calls': total_dynamic_calls,
            'high_risk_calls': high_risk_count,
            'security_score': security_score,
            'risk_level': self._determine_overall_risk(security_score, high_risk_count),
            'recommendations': self._generate_risk_recommendations(
                eval_calls, reflection_calls, dynamic_imports
            )
        }
    
    def _calculate_security_score(self, eval_calls: List[Dict[str, Any]], 
                                 reflection_calls: List[Dict[str, Any]],
                                 dynamic_imports: List[Dict[str, Any]],
                                 computed_access: List[Dict[str, Any]],
                                 indirect_calls: List[Dict[str, Any]],
                                 template_calls: List[Dict[str, Any]]) -> float:
        """Calculate a security score based on dynamic call patterns."""
        score = 100.0  # Start with perfect score
        
        # Penalties for different types of dynamic calls
        penalties = {
            'direct_eval': 20,
            'function_constructor': 15,
            'string_timeout': 10,
            'proxy': 10,
            'reflect_api': 5,
            'property_definition': 8,
            'dynamic_import_variable': 3,
            'computed_property': 2,
            'indirect_call': 1,
            'template_call': 2
        }
        
        # Apply penalties
        for call in eval_calls:
            score -= penalties.get(call['type'], 5)
        
        for call in reflection_calls:
            score -= penalties.get(call['type'], 2)
        
        for call in dynamic_imports:
            if call.get('is_variable'):
                score -= penalties['dynamic_import_variable']
        
        score -= len(computed_access) * penalties['computed_property']
        score -= len(indirect_calls) * penalties['indirect_call']
        score -= len(template_calls) * penalties['template_call']
        
        return max(0.0, score)
    
    def _determine_overall_risk(self, security_score: float, high_risk_count: int) -> str:
        """Determine overall risk level."""
        if high_risk_count > 0 or security_score < 50:
            return 'high'
        elif security_score < 80:
            return 'medium'
        else:
            return 'low'
    
    def _generate_risk_recommendations(self, eval_calls: List[Dict[str, Any]], 
                                     reflection_calls: List[Dict[str, Any]],
                                     dynamic_imports: List[Dict[str, Any]]) -> List[str]:
        """Generate security recommendations."""
        recommendations = []
        
        if any(call['type'] in ['direct_eval', 'function_constructor'] for call in eval_calls):
            recommendations.append("Avoid using eval() or Function() constructor for security reasons")
        
        if any(call['type'] == 'proxy' for call in reflection_calls):
            recommendations.append("Consider using safer alternatives to Proxy for metaprogramming")
        
        if any(call.get('is_variable') for call in dynamic_imports):
            recommendations.append("Use static import paths when possible to improve bundling and security")
        
        if eval_calls or reflection_calls:
            recommendations.append("Consider using TypeScript's type system to reduce need for dynamic calls")
            recommendations.append("Implement proper input validation for any dynamic code execution")
        
        return recommendations


class CallPatternAnalyzer:
    """Analyzes patterns in dynamic calls to identify potential issues."""
    
    def __init__(self):
        """Initialize the pattern analyzer."""
        self.call_patterns = defaultdict(list)
        self.suspicious_patterns = []
    
    def analyze_call_patterns(self, dynamic_calls: Dict[str, Any]) -> Dict[str, Any]:
        """Analyze patterns in dynamic calls."""
        patterns = {
            'repeated_eval_contexts': self._find_repeated_eval_contexts(dynamic_calls.get('eval_calls', [])),
            'import_path_patterns': self._analyze_import_path_patterns(dynamic_calls.get('dynamic_imports', [])),
            'property_access_patterns': self._analyze_property_access_patterns(dynamic_calls.get('computed_property_access', [])),
            'potential_vulnerabilities': self._identify_vulnerabilities(dynamic_calls)
        }
        
        return patterns
    
    def _find_repeated_eval_contexts(self, eval_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """Find repeated contexts where eval is used."""
        context_counts = defaultdict(list)
        
        for call in eval_calls:
            # Group by similar contexts (simplified version)
            context_key = call['context'][:50]  # First 50 chars as key
            context_counts[context_key].append(call)
        
        repeated = []
        for context, calls in context_counts.items():
            if len(calls) > 1:
                repeated.append({
                    'context_pattern': context,
                    'occurrences': len(calls),
                    'lines': [call['line'] for call in calls]
                })
        
        return repeated
    
    def _analyze_import_path_patterns(self, dynamic_imports: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Analyze patterns in dynamic import paths."""
        patterns = {
            'variable_imports': len([imp for imp in dynamic_imports if imp.get('is_variable')]),
            'computed_imports': len([imp for imp in dynamic_imports if imp.get('is_computed')]),
            'common_expressions': self._find_common_import_expressions(dynamic_imports)
        }
        
        return patterns
    
    def _analyze_property_access_patterns(self, computed_access: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Analyze patterns in computed property access."""
        object_counts = defaultdict(int)
        expression_types = defaultdict(int)
        
        for access in computed_access:
            object_counts[access['object']] += 1
            if access.get('is_computed'):
                expression_types['computed'] += 1
            elif access.get('is_string_literal'):
                expression_types['string_literal'] += 1
            else:
                expression_types['variable'] += 1
        
        return {
            'objects_with_computed_access': dict(object_counts),
            'expression_type_distribution': dict(expression_types),
            'highly_dynamic_objects': {obj: count for obj, count in object_counts.items() if count > 5}
        }
    
    def _find_common_import_expressions(self, dynamic_imports: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """Find common expressions used in dynamic imports."""
        expressions = defaultdict(int)
        
        for imp in dynamic_imports:
            expr = imp['module_expression']
            # Normalize expression for pattern matching
            normalized = re.sub(r'[\'"`]', '', expr)
            expressions[normalized] += 1
        
        common = [{'expression': expr, 'count': count} 
                 for expr, count in expressions.items() if count > 1]
        
        return sorted(common, key=lambda x: x['count'], reverse=True)
    
    def _identify_vulnerabilities(self, dynamic_calls: Dict[str, Any]) -> List[Dict[str, Any]]:
        """Identify potential security vulnerabilities."""
        vulnerabilities = []
        
        # Check for dangerous eval usage
        for call in dynamic_calls.get('eval_calls', []):
            if call.get('risk_level') == 'high':
                vulnerabilities.append({
                    'type': 'code_injection',
                    'description': f"Dangerous eval usage at line {call['line']}",
                    'severity': 'high',
                    'line': call['line']
                })
        
        # Check for unsafe dynamic imports
        for imp in dynamic_calls.get('dynamic_imports', []):
            if imp.get('is_variable') and imp.get('is_computed'):
                vulnerabilities.append({
                    'type': 'module_injection',
                    'description': f"Dynamic import with computed path at line {imp['line']}",
                    'severity': 'medium',
                    'line': imp['line']
                })
        
        # Check for prototype pollution risks
        reflection_calls = dynamic_calls.get('reflection_calls', [])
        dangerous_reflection = [call for call in reflection_calls 
                               if call.get('type') in ['property_definition', 'prototype_access']]
        
        for call in dangerous_reflection:
            vulnerabilities.append({
                'type': 'prototype_pollution',
                'description': f"Potentially unsafe reflection at line {call['line']}",
                'severity': 'medium',
                'line': call['line']
            })
        
        return vulnerabilities